import { nameof } from "@ts-morph/common";
import { expect } from "chai";
import { ClassDeclaration, FunctionDeclaration, TypeParameterDeclaration, TypeParameterVariance } from "../../../../compiler";
import { StructureKind, TypeParameterDeclarationStructure } from "../../../../structures";
import { WriterFunction } from "../../../../types";
import { MakeRequired } from "../../../../typings";
import { getInfoFromText, OptionalTrivia } from "../../testHelpers";

describe("TypeParameterDeclaration", () => {
  function getTypeParameterFromText(text: string, index = 0) {
    const { firstChild } = getInfoFromText<FunctionDeclaration>(text);
    return firstChild.getTypeParameters()[index];
  }

  describe(nameof<TypeParameterDeclaration>("getName"), () => {
    it("should get the name", () => {
      const typeParameterDeclaration = getTypeParameterFromText("function func<T>() {}\n");
      expect(typeParameterDeclaration.getName()).to.equal("T");
    });
  });

  describe(nameof<TypeParameterDeclaration>("setIsConst"), () => {
    function doTest(text: string, value: boolean, expected: string) {
      const typeParameterDeclaration = getTypeParameterFromText(text);
      typeParameterDeclaration.setIsConst(value);
      expect(typeParameterDeclaration.getSourceFile().getFullText()).to.equal(expected);

      // now do the opposite and it should equal the original text
      typeParameterDeclaration.setIsConst(!value);
      expect(typeParameterDeclaration.getSourceFile().getFullText()).to.equal(text);
    }

    it("should set and remove when it doesn't exist", () => {
      doTest("function func<T>() {}", true, "function func<const T>() {}");
      doTest("function func<in T>() {}", true, "function func<const in T>() {}");
      doTest("function func<out T>() {}", true, "function func<const out T>() {}");
      doTest("function func<in out T>() {}", true, "function func<const in out T>() {}");
    });
  });

  describe(nameof<TypeParameterDeclaration>("isConst"), () => {
    function doTest(text: string, expectedValue: boolean) {
      const typeParameterDeclaration = getTypeParameterFromText(text);
      expect(typeParameterDeclaration.isConst()).to.equal(expectedValue);
    }

    it("should get", () => {
      doTest("function func<T>() {}", false);
      doTest("function func<in T>() {}", false);
      doTest("function func<out T>() {}", false);
      doTest("function func<in out T>() {}", false);
      doTest("function func<const T>() {}", true);
      doTest("function func<const out T>() {}", true);
      doTest("function func<const in T>() {}", true);
      doTest("function func<const in out T>() {}", true);
    });
  });

  describe(nameof<TypeParameterDeclaration>("getConstraint"), () => {
    it("should return undefined when there's no constraint", () => {
      const typeParameterDeclaration = getTypeParameterFromText("function func<T>() {}\n");
      expect(typeParameterDeclaration.getConstraint()).to.be.undefined;
    });

    it("should return the constraint type node when there's a constraint", () => {
      const typeParameterDeclaration = getTypeParameterFromText("function func<T extends string>() {}\n");
      expect(typeParameterDeclaration.getConstraint()!.getText()).to.equal("string");
    });
  });

  describe(nameof<TypeParameterDeclaration>("getConstraintOrThrow"), () => {
    it("should throw when there's no constraint", () => {
      const typeParameterDeclaration = getTypeParameterFromText("function func<T>() {}\n");
      expect(() => typeParameterDeclaration.getConstraintOrThrow()).to.throw();
    });

    it("should return the constraint type node when there's a constraint", () => {
      const typeParameterDeclaration = getTypeParameterFromText("function func<T extends string>() {}\n");
      expect(typeParameterDeclaration.getConstraintOrThrow().getText()).to.equal("string");
    });
  });

  describe(nameof<TypeParameterDeclaration>("setConstraint"), () => {
    function doTest(text: string, name: string | WriterFunction, expected: string) {
      const typeParameterDeclaration = getTypeParameterFromText(text);
      typeParameterDeclaration.setConstraint(name);
      expect(typeParameterDeclaration._sourceFile.getFullText()).to.equal(expected);
    }

    it("should set when it doesn't exist", () => {
      doTest("function func<T>() {}", "string", "function func<T extends string>() {}");
    });

    it("should set on multiple lines", () => {
      doTest("function func<T>() {}", writer => writer.writeLine("string |").write("number"), "function func<T extends string |\n    number>() {}");
    });

    it("should set when it exists", () => {
      doTest("function func<T extends number>() {}", "string", "function func<T extends string>() {}");
    });

    it("should remove when passing in an empty string", () => {
      doTest("function func<T extends number>() {}", "", "function func<T>() {}");
    });

    it("should set when it has a default exists", () => {
      doTest("function func<T = number>() {}", "string", "function func<T extends string = number>() {}");
    });
  });

  describe(nameof<TypeParameterDeclaration>("removeConstraint"), () => {
    function doTest(text: string, expected: string) {
      const typeParameterDeclaration = getTypeParameterFromText(text);
      typeParameterDeclaration.removeConstraint();
      expect(typeParameterDeclaration._sourceFile.getFullText()).to.equal(expected);
    }

    it("should do nothing when it doesn't exist", () => {
      doTest("function func<T = string>() {}", "function func<T = string>() {}");
    });

    it("should remove when it exists", () => {
      doTest("function func<T extends string>() {}", "function func<T>() {}");
    });

    it("should remove when it and a default exists", () => {
      doTest("function func<T extends string = string>() {}", "function func<T = string>() {}");
    });
  });

  describe(nameof<TypeParameterDeclaration>("getDefault"), () => {
    it("should return undefined when there's no default node", () => {
      const typeParameterDeclaration = getTypeParameterFromText("function func<T>() {}\n");
      expect(typeParameterDeclaration.getDefault()).to.be.undefined;
    });

    it("should return the default type node when there's a default", () => {
      const typeParameterDeclaration = getTypeParameterFromText("function func<T = string>() {}\n");
      expect(typeParameterDeclaration.getDefault()!.getText()).to.equal("string");
    });
  });

  describe(nameof<TypeParameterDeclaration>("getDefault"), () => {
    it("should throw when there's no default node", () => {
      const typeParameterDeclaration = getTypeParameterFromText("function func<T>() {}\n");
      expect(() => typeParameterDeclaration.getDefaultOrThrow()).to.throw();
    });

    it("should return the default type node when there's a default", () => {
      const typeParameterDeclaration = getTypeParameterFromText("function func<T = string>() {}\n");
      expect(typeParameterDeclaration.getDefaultOrThrow().getText()).to.equal("string");
    });
  });

  describe(nameof<TypeParameterDeclaration>("setDefault"), () => {
    function doTest(text: string, name: string | WriterFunction, expected: string) {
      const typeParameterDeclaration = getTypeParameterFromText(text);
      typeParameterDeclaration.setDefault(name);
      expect(typeParameterDeclaration._sourceFile.getFullText()).to.equal(expected);
    }

    it("should set when it doesn't exist", () => {
      doTest("function func<T>() {}", "string", "function func<T = string>() {}");
    });

    it("should set on multiple lines", () => {
      doTest("function func<T>() {}", writer => writer.writeLine("string |").write("number"), "function func<T = string |\n    number>() {}");
    });

    it("should set when it exists", () => {
      doTest("function func<T = number>() {}", "string", "function func<T = string>() {}");
    });

    it("should remove when passing in an empty string", () => {
      doTest("function func<T = number>() {}", "", "function func<T>() {}");
    });

    it("should set when it has a constraint exists", () => {
      doTest("function func<T extends number>() {}", "string", "function func<T extends number = string>() {}");
    });
  });

  describe(nameof<TypeParameterDeclaration>("removeDefault"), () => {
    function doTest(text: string, expected: string) {
      const typeParameterDeclaration = getTypeParameterFromText(text);
      typeParameterDeclaration.removeDefault();
      expect(typeParameterDeclaration._sourceFile.getFullText()).to.equal(expected);
    }

    it("should do nothing when it doesn't exist", () => {
      doTest("function func<T extends string>() {}", "function func<T extends string>() {}");
    });

    it("should remove when it exists", () => {
      doTest("function func<T = string>() {}", "function func<T>() {}");
    });

    it("should remove when it and a constraint exists", () => {
      doTest("function func<T extends string = string>() {}", "function func<T extends string>() {}");
    });
  });

  describe(nameof<TypeParameterDeclaration>("setVariance"), () => {
    function doTest(text: string, variance: TypeParameterVariance, expected: string) {
      const typeParameterDeclaration = getTypeParameterFromText(text);
      typeParameterDeclaration.setVariance(variance);
      expect(typeParameterDeclaration.getSourceFile().getFullText()).to.equal(expected);
    }

    it("should set when it doesn't exist", () => {
      doTest("function func<T>() {}", TypeParameterVariance.Out, "function func<out T>() {}");
      doTest("function func<T>() {}", TypeParameterVariance.In, "function func<in T>() {}");
      doTest("function func<T>() {}", TypeParameterVariance.InOut, "function func<in out T>() {}");
    });

    it("should change", () => {
      doTest("function func<in T>() {}", TypeParameterVariance.Out, "function func<out T>() {}");
      doTest("function func<out T>() {}", TypeParameterVariance.In, "function func<in T>() {}");
      doTest("function func<in out T>() {}", TypeParameterVariance.In, "function func<in T>() {}");
      doTest("function func<in out T>() {}", TypeParameterVariance.Out, "function func<out T>() {}");
    });

    it("should remove", () => {
      doTest("function func<in T>() {}", TypeParameterVariance.None, "function func<T>() {}");
      doTest("function func<out T>() {}", TypeParameterVariance.None, "function func<T>() {}");
      doTest("function func<in out T>() {}", TypeParameterVariance.None, "function func<T>() {}");
    });
  });

  describe(nameof<TypeParameterDeclaration>("getVariance"), () => {
    function doTest(text: string, expectedVariance: TypeParameterVariance) {
      const typeParameterDeclaration = getTypeParameterFromText(text);
      expect(typeParameterDeclaration.getVariance()).to.equal(expectedVariance);
    }

    it("should get", () => {
      doTest("function func<T>() {}", TypeParameterVariance.None);
      doTest("function func<in T>() {}", TypeParameterVariance.In);
      doTest("function func<out T>() {}", TypeParameterVariance.Out);
      doTest("function func<in out T>() {}", TypeParameterVariance.InOut);
    });
  });

  describe(nameof<TypeParameterDeclaration>("remove"), () => {
    function doTest(startText: string, indexToRemove: number, expectedText: string) {
      const typeParameterDeclaration = getTypeParameterFromText(startText, indexToRemove);
      const { _sourceFile } = typeParameterDeclaration;

      typeParameterDeclaration.remove();

      expect(_sourceFile.getFullText()).to.equal(expectedText);
    }

    it("should remove when its the only type parameter", () => {
      doTest("function func<T>() {}", 0, "function func() {}");
    });

    it("should remove when it's the first type parameter", () => {
      doTest("function func<T, U>() {}", 0, "function func<U>() {}");
    });

    it("should remove when it's the middle type parameter", () => {
      doTest("function func<T, U, V>() {}", 1, "function func<T, V>() {}");
    });

    it("should remove when it's the last type parameter", () => {
      doTest("function func<T, U>() {}", 1, "function func<T>() {}");
    });

    it("should remove when it has a constraint", () => {
      doTest("function func<T extends Other, U>() {}", 0, "function func<U>() {}");
    });
  });

  describe(nameof<TypeParameterDeclaration>("set"), () => {
    function doTest(text: string, structure: Partial<TypeParameterDeclarationStructure>, expectedText: string) {
      const { sourceFile } = getInfoFromText<ClassDeclaration>(text);
      sourceFile.getClasses()[0].getTypeParameters()[0].set(structure);
      expect(sourceFile.getFullText()).to.equal(expectedText);
    }

    it("should not change when empty", () => {
      const code = "class C<in T extends string = number> {}";
      doTest(code, {}, code);
    });

    it("should remove when specifying undefined for constraint and default", () => {
      doTest("class C<T extends string = number> {}", { constraint: undefined, default: undefined }, "class C<T> {}");
    });

    it("should replace existing", () => {
      doTest("class C<T extends string = number> {}", { name: "U", constraint: "number", default: "string" }, "class C<U extends number = string> {}");
    });

    it("should add constraint and default when not exists", () => {
      doTest("class C<T> {}", { name: "U", constraint: "number", default: "string" }, "class C<U extends number = string> {}");
    });

    it("should add constraint and default properly when they span multiple lines", () => {
      doTest(
        "class C<T> {}",
        { name: "U", constraint: "{\n    prop: string;\n}", default: "{\n    other: number;\n}" },
        "class C<U extends {\n        prop: string;\n    } = {\n        other: number;\n    }> {}",
      );
    });

    it("should change variance", () => {
      doTest(
        "class C<in T extends string = number> {}",
        { variance: TypeParameterVariance.Out },
        "class C<out T extends string = number> {}",
      );
    });
  });

  describe(nameof<TypeParameterDeclaration>("getStructure"), () => {
    function doTest(text: string, expectedStructure: OptionalTrivia<MakeRequired<TypeParameterDeclarationStructure>>) {
      const { firstChild } = getInfoFromText<ClassDeclaration>(text);
      const structure = firstChild.getTypeParameters()[0].getStructure();
      expect(structure).to.deep.equal(expectedStructure);
    }

    it("should get when it has nothing", () => {
      doTest("class C<T> {}", {
        kind: StructureKind.TypeParameter,
        name: "T",
        isConst: false,
        constraint: undefined,
        default: undefined,
        variance: TypeParameterVariance.None,
      });
    });

    it("should get when it has everything", () => {
      doTest("class C<const in T extends string = number> {}", {
        kind: StructureKind.TypeParameter,
        name: "T",
        isConst: true,
        constraint: "string",
        default: "number",
        variance: TypeParameterVariance.In,
      });
    });

    it("should trim leading indentation on the contraint and default", () => {
      doTest("class C<T extends {\n        prop: string;\n    } = {\n    }> {}", {
        kind: StructureKind.TypeParameter,
        name: "T",
        isConst: false,
        constraint: "{\n    prop: string;\n}",
        default: "{\n}",
        variance: TypeParameterVariance.None,
      });
    });
  });
});
